/* * DragAndDropPanel.java * * Created on 10 September 2006, 21:15 */ package uk.co.bytemark.vm.enigma.inquisition.gui.quiz; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Font; import java.awt.Point; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.dnd.DnDConstants; import java.awt.dnd.DragGestureEvent; import java.awt.dnd.DragGestureListener; import java.awt.dnd.DragSource; import java.awt.dnd.DragSourceAdapter; import java.awt.dnd.DragSourceContext; import java.awt.dnd.DragSourceDragEvent; import java.awt.dnd.DragSourceDropEvent; import java.awt.dnd.DragSourceListener; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetAdapter; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; import java.awt.dnd.DropTargetListener; import java.awt.dnd.InvalidDnDOperationException; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JComponent; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.text.AttributeSet; import javax.swing.text.Document; import javax.swing.text.Element; import javax.swing.text.StyleConstants; import javax.swing.text.View; import javax.swing.text.ViewFactory; import javax.swing.text.html.FormView; import javax.swing.text.html.HTML; import javax.swing.text.html.HTMLDocument; import javax.swing.text.html.HTMLEditorKit; import uk.co.bytemark.vm.enigma.inquisition.questions.Answer; import uk.co.bytemark.vm.enigma.inquisition.questions.DragAndDropAnswer; import uk.co.bytemark.vm.enigma.inquisition.questions.DragAndDropQuestion; import uk.co.bytemark.vm.enigma.inquisition.questions.DragAndDropRenderingHelper; /** * * A {@link QuestionPanel} for handling {@link DragAndDropQuestion}s. A <tt>DragAndDropPanel</tt> allows a user to * drag fragments of text around on a document containing slots. */ public class DragAndDropPanel extends QuestionPanel { private static final Logger LOGGER = Logger.getLogger(DragAndDropPanel.class.getName()); public static final Color CORRECT_COLOUR = new Color(0.9f, 1.0f, 0.9f); public static final Color INCORRECT_COLOUR = new Color(1.0f, 0.9f, 0.9f); public static final Color NORMAL_COLOUR = new Color(1.0f, 1.0f, 0.8f); private final DragAndDropQuestion question; private final QuizFrame quizFrame; private final TransparentPane transparentPane; private final DragAndDropTextFieldManager textFieldManager = new DragAndDropTextFieldManager(); private final DragAndDropFragmentBinPanel fragmentPanel; private boolean dragsDisabled = false; private boolean inQuestionMode = true; private final AnswerChangedObserver answerChangedObserver; /** Creates new form DragAndDropPanel */ public DragAndDropPanel(DragAndDropQuestion question, QuizFrame quizFrame, AnswerChangedObserver answerChangedObserver, TransparentPane transparentPane) { this.question = question; this.quizFrame = quizFrame; this.transparentPane = transparentPane; this.answerChangedObserver = answerChangedObserver; initComponents(); textFieldManager.blankAllTextFields(); // TODO: Fix cast this.fragmentPanel = (DragAndDropFragmentBinPanel) optionsPanel; // Make sure the question is scrolled to the top if necessary questionTextPane.setCaretPosition(0); // Generic drag-over listeners so that a dragged image moves over these // components new DropTarget(questionTextPane, new DragOnMeDropListener(questionTextPane)); new DropTarget(fragmentPanel, new DragOnMeDropListener(fragmentPanel)); // Recognize drags starting from the fragments bin DragSource dragSource = new DragSource(); dragSource.createDefaultDragGestureRecognizer(fragmentPanel, DnDConstants.ACTION_COPY, new FragmentsBinDragGestureListener(dragSource)); } /** * A listener to recognize drags originating from the fragments bin. */ class FragmentsBinDragGestureListener implements DragGestureListener { private DragSource dragSource; FragmentsBinDragGestureListener(DragSource dragSource) { this.dragSource = dragSource; } public void dragGestureRecognized(DragGestureEvent event) { if (dragsDisabled) return; // Get a fragment, if there's one there String dragText = fragmentPanel.getFragmentAtPoint(event.getDragOrigin()); if (dragText == null) return; // Put the fragment text as the transferable StringSelection transferable = new StringSelection(dragText); // Hide the fragment if (!question.canReuseFragments()) fragmentPanel.hideFragment(dragText); // Set up the fragment as the dragged image BufferedImage image = fragmentPanel.getFragmentImage(dragText); if (image == null) return; // Activate the transparent pane and set it up with the appropriate dragged image Point point = (Point) event.getDragOrigin().clone(); SwingUtilities.convertPointToScreen(point, fragmentPanel); SwingUtilities.convertPointFromScreen(point, transparentPane); transparentPane.activate(point, image); transparentPane.setVisible(true); // Start the drag and set up source responses to dragging DragSourceListener dsl = new FragmentBinDragSourceListener(dragText); dragSource.startDrag(event, DragSource.DefaultCopyDrop, transferable, dsl); } } /** * A drag source listener to handle in-progress drag events that have started from the fragment bin */ class FragmentBinDragSourceListener extends DragSourceAdapter { private String fragment; FragmentBinDragSourceListener(String fragment) { this.fragment = fragment; } @Override public void dragDropEnd(DragSourceDropEvent dsde) { // The drag has ended, so we close down the transparentPane answerChangedObserver.answerChanged(getAnswer()); if (dsde.getDropSuccess()) { transparentPane.setVisible(false); } else { // If was unsuccessful, then we // need to put the fragment back into the bin transparentPane.sendBackToOrigin(new TransparentPane.Callback() { public void returnedToOrigin() { fragmentPanel.showFragment(fragment); } }); } } // This avoids cursor flicker @Override public void dragOver(DragSourceDragEvent dsde) { DragSourceContext context = dsde.getDragSourceContext(); context.setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); } } /** * A drag listener that listens for dragOver events and redraws the dragged image as the user moves over the * component. */ class DragOnMeDropListener extends DropTargetAdapter { private JComponent component; DragOnMeDropListener(JComponent component) { this.component = component; } public void drop(DropTargetDropEvent event) { // We don't care about drop events here (TODO: why not?) } @Override public void dragOver(DropTargetDragEvent event) { Point point = (Point) event.getLocation().clone(); SwingUtilities.convertPointToScreen(point, component); SwingUtilities.convertPointFromScreen(point, transparentPane); transparentPane.setPosition(point); transparentPane.repaint(); } } boolean dragsAreDisabled() { return dragsDisabled; } @Override public Answer getAnswer() { return textFieldManager.getAnswer(); } @Override public void enterReviewMode() { if (!inQuestionMode) return; inQuestionMode = false; textFieldManager.colourTextFieldsAccordingToCorrectness(); dragsDisabled = true; fragmentPanel.setActive(false); } @Override void enterQuestionMode() { if (inQuestionMode) return; inQuestionMode = true; textFieldManager.colourTextFieldsAccordingToWhetherTheyAreEmptyOrNot(); dragsDisabled = false; fragmentPanel.setActive(true); } @Override public DragAndDropQuestion getQuestion() { return question; } @Override public String getExplanationText() { return DragAndDropRenderingHelper.getExplanationText(question); } /** * This method is called from within the constructor to initialise the form. WARNING: Do NOT modify this code. The * content of this method is always regenerated by the Form Editor. */ // <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:initComponents private void initComponents() { optionsPanel = new DragAndDropFragmentBinPanel(question.getFragments()); questionTextScrollPane = new javax.swing.JScrollPane(); questionTextPane = new javax.swing.JTextPane(); optionsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(DragAndDropRenderingHelper .optionsTitle(question))); org.jdesktop.layout.GroupLayout optionsPanelLayout = new org.jdesktop.layout.GroupLayout(optionsPanel); optionsPanel.setLayout(optionsPanelLayout); optionsPanelLayout.setHorizontalGroup(optionsPanelLayout.createParallelGroup( org.jdesktop.layout.GroupLayout.LEADING).add(0, 420, Short.MAX_VALUE)); optionsPanelLayout.setVerticalGroup(optionsPanelLayout.createParallelGroup( org.jdesktop.layout.GroupLayout.LEADING).add(0, 271, Short.MAX_VALUE)); questionTextPane.setContentType("text/html"); questionTextPane.setEditable(false); questionTextPane.setEditorKit(new DragAndDropSlotHTMLEditorKit()); questionTextPane.setText(DragAndDropRenderingHelper.getQuestionText(question, true)); questionTextScrollPane.setViewportView(questionTextPane); org.jdesktop.layout.GroupLayout layout = new org.jdesktop.layout.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING).add( questionTextScrollPane, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 430, Short.MAX_VALUE).add( optionsPanel, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)); layout.setVerticalGroup(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING).add( layout.createSequentialGroup().add(questionTextScrollPane, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 296, Short.MAX_VALUE).addPreferredGap( org.jdesktop.layout.LayoutStyle.RELATED).add(optionsPanel, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE))); }// </editor-fold>//GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JPanel optionsPanel; private javax.swing.JTextPane questionTextPane; private javax.swing.JScrollPane questionTextScrollPane; // End of variables declaration//GEN-END:variables /** * An HTMLEditorKit that customises <INPUT> creation for a <tt>JTextField</tt> loaded with HTML. */ public class DragAndDropSlotHTMLEditorKit extends HTMLEditorKit { private class MyHTMLFactory extends HTMLFactory { @Override public View create(Element element) { AttributeSet attributes = element.getAttributes(); if (attributes.getAttribute(StyleConstants.NameAttribute) == HTML.Tag.INPUT) { return new DragAndDropSlotFormView(element); } else { return super.create(element); } } } @Override public Document createDefaultDocument() { return new HTMLDocument(getStyleSheet()); } @Override public ViewFactory getViewFactory() { return new MyHTMLFactory(); } } /** * A class that lets us intercept the creation of JTextfields created to render HTML INPUT tags, so that we can * adapt them as drag and drop slots; namely, make them non-editable, colour them, and make them respond to drag * events. */ private class DragAndDropSlotFormView extends FormView { public DragAndDropSlotFormView(Element elem) { super(elem); } @Override protected Component createComponent() { Component component = super.createComponent(); AttributeSet attributes = getElement().getAttributes(); HTML.Tag htmlTag = (HTML.Tag) attributes.getAttribute(StyleConstants.NameAttribute); if (htmlTag == HTML.Tag.INPUT) { int id = Integer.parseInt((String) attributes.getAttribute(HTML.Attribute.ID)); String slotText = (String) attributes.getAttribute(HTML.Attribute.VALUE); JTextField textField = (JTextField) component; textField.setBackground(NORMAL_COLOUR); textField.setEditable(false); // int oldSize = tf.getFont().getSize(); // tf.setFont(new Font("Monospaced", Font.PLAIN, oldSize)); textField.setFont(new Font("Monospaced", Font.PLAIN, 12)); // System.out.println(elem.getStartOffset() + ", " + elemText); textFieldManager.addTextField(id, slotText, textField); // Set up incoming drags new DropTarget(textField, new TextFieldDropTargetListener(textField)); // Recognise new drags that start from this textField DragSource dragSource = new DragSource(); dragSource.createDefaultDragGestureRecognizer(textField, DnDConstants.ACTION_MOVE, new TextFieldDragGestureRecognizer(textField, dragSource)); } return component; } } /** * Handles drops and drag-overs/exits on a text field. */ private class TextFieldDropTargetListener implements DropTargetListener { private JTextField textField; private String swappedOutText = ""; TextFieldDropTargetListener(JTextField textField) { this.textField = textField; } /** * On entering, temporarily swaps out the current text and puts in the drag fragment to show how this fragment * would look. */ public void dragEnter(DropTargetDragEvent event) { swappedOutText = textField.getText(); String dragText = getStringFromTransferable(event.getTransferable()); if (dragText != null) { textField.setText(dragText); textField.setBackground(DragAndDropFragmentBinPanel.FRAGMENT_FILL_COLOUR); } } /** * Restores previous text (or blankness). */ public void dragExit(DropTargetEvent event) { textField.setText(swappedOutText); if (swappedOutText.equals("")) { textField.setBackground(NORMAL_COLOUR); } swappedOutText = ""; quizFrame.setGlassPaneVisible(true); } /** * Puts the fragment in this slot, and bumps any fragment already there back to the bin. */ public void drop(final DropTargetDropEvent event) { try { String dragText = getStringFromTransferable(event.getTransferable()); if (dragText == null) { event.rejectDrop(); } else { textField.setText(dragText); if (!swappedOutText.equals("")) { // put the swapped-out fragment back into the bin Point originPoint = new Point(fragmentPanel.getWidth() / 2, fragmentPanel.getHeight() / 2); SwingUtilities.convertPointToScreen(originPoint, fragmentPanel); SwingUtilities.convertPointFromScreen(originPoint, transparentPane); transparentPane.setVisible(true); transparentPane.sendBackToOrigin(new TransparentPane.Callback() { public void returnedToOrigin() { fragmentPanel.showFragment(swappedOutText); } }, originPoint, fragmentPanel.getFragmentImage(swappedOutText)); } event.dropComplete(true); answerChangedObserver.answerChanged(getAnswer()); } } catch (InvalidDnDOperationException e) { LOGGER.log(Level.WARNING, "Drag and drop problem", e); event.rejectDrop(); } } /** * When dragging over this textField, disable the transparent drag pane, because we are rendering the fragment * as "snapped" into the textfield. */ public void dragOver(DropTargetDragEvent dtde) { quizFrame.setGlassPaneVisible(false); } public void dropActionChanged(DropTargetDragEvent dtde) { // TODO: What's this, then? } private String getStringFromTransferable(Transferable tr) { DataFlavor[] flavors = tr.getTransferDataFlavors(); if (flavors.length >= 1 && flavors[0].isFlavorTextType()) { try { return (String) tr.getTransferData(flavors[0]); } catch (IOException e) { LOGGER.log(Level.WARNING, "IO problem", e); return null; } catch (UnsupportedFlavorException e) { LOGGER.log(Level.WARNING, "Weird drag flavour problem", e); return null; } } else { return null; } } } /** * A recognizer for drags initiating from a text field. */ private class TextFieldDragGestureRecognizer implements DragGestureListener { JTextField textField; DragSource dragSource; TextFieldDragGestureRecognizer(JTextField textField, DragSource dragSource) { this.textField = textField; this.dragSource = dragSource; } public void dragGestureRecognized(DragGestureEvent event) { if (dragsDisabled) return; final String dragText = textField.getText(); // Don't initiate any drag if there's nothing in the slot if (dragText.equals("")) return; // If there's a large mouse move, then it seems that DropTargetListener // dragExit() never gets fired on the textField, so we do what gets done in dragExit here just in // case. However, if there's a small mouse move after this, a "dragEnter" gets // fired on the textField, restoring the text and setting the background. The visual effect // is of a momentary flicker. Not sure how to get around this. textField.setText(""); textField.setBackground(NORMAL_COLOUR); // testQuestionFrame.answerChanged(); StringSelection transferable = new StringSelection(dragText); // Set up the fragment as the dragged ghost image BufferedImage image = fragmentPanel.getFragmentImage(dragText); assert (image != null); // Activate the transparent pane and set it up with the appropriate dragged // image Point point = (Point) event.getDragOrigin().clone(); SwingUtilities.convertPointToScreen(point, textField); SwingUtilities.convertPointFromScreen(point, transparentPane); transparentPane.activate(point, image); // TODO: merge this with DragAndDropPanel.FragmentBinDragSourceListener dragSource.startDrag(event, DragSource.DefaultMoveDrop, transferable, new DragSourceAdapter() { // Stops cursor flicker @Override public void dragOver(DragSourceDragEvent dragSourceDragEvent) { DragSourceContext context = dragSourceDragEvent.getDragSourceContext(); context.setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); } @Override public void dragDropEnd(DragSourceDropEvent dragSourceDropEvent) { answerChangedObserver.answerChanged(getAnswer()); if (dragSourceDropEvent.getDropSuccess()) { transparentPane.setVisible(false); } else { // If was unsuccessful, then we need to put the fragment back into the bin Point originPoint = new Point(fragmentPanel.getWidth() / 2, fragmentPanel.getHeight() / 2); SwingUtilities.convertPointToScreen(originPoint, fragmentPanel); SwingUtilities.convertPointFromScreen(originPoint, transparentPane); transparentPane.sendBackToOrigin(new TransparentPane.Callback() { public void returnedToOrigin() { fragmentPanel.showFragment(dragText); } }, originPoint); } } }); } } @Override public void setAnswer(Answer generalAnswer) { DragAndDropAnswer answer = (DragAndDropAnswer) generalAnswer; List<String> slotAnswers = answer.getSlotAnswers(); if (!question.canReuseFragments()) { for (String fragment : question.getFragments()) { if (slotAnswers.contains(fragment)) { fragmentPanel.hideFragment(fragment); } else { fragmentPanel.showFragment(fragment); } } } textFieldManager.setFields(slotAnswers); if (inQuestionMode) { textFieldManager.colourTextFieldsAccordingToWhetherTheyAreEmptyOrNot(); } else { textFieldManager.colourTextFieldsAccordingToCorrectness(); } } }